/*	$Id: cookie.c 1056 2010-04-21 03:51:21Z phrakt $	*/
/*
 * Copyright (c) 2008 Jean-Francois Brousseau <phrakt@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
/*
 * HTTP Cookie API
 *
 * The following code provides an Application Programming Interface to
 * manipulate HTTP cookies.  It provides functions to parse cookie strings
 * and format cookie structures into their corresponding strings.  The API
 * is built around the http_cookie structure (http_cookie_t), which is opaque
 * outside of this library.
 *
 * This interface attempts to conform to the HTTP cookie specification as
 * described in "RFC2965: HTTP State Management Mechanism".
 *
 */

#include <sys/types.h>
#include <sys/time.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <canopy/log.h>
#include <canopy/mem.h>
#include <canopy/string.h>

#include "msg.h"
#include "date.h"
#include "http.h"
#include "cookie.h"
#include "private.h"


#define TOK_INVAL		-1	
#define TOK_END			0
#define TOK_EQUAL		1
#define TOK_SCOL		2
#define TOK_STRING		3
#define TOK_QSTRING		4


#define http_istokchar(c)						 \
	(((c) > 0x19) && ((c) < 0x7f) && ((c) != ';') && ((c) != '"') && \
	 ((c) != '(') && ((c) != ')') && ((c) != '<') && ((c) != '>') && \
	 ((c) != '@') && ((c) != '=') && ((c) != '\\') && ((c) != '"') && \
	 ((c) != '?') && ((c) != '{') && ((c) != '}'))





struct http_cookie {
	char           *ck_name;
	char           *ck_value;
	char           *ck_comment;
	char           *ck_comment_url;
	char           *ck_domain;
	char           *ck_path;

	int             ck_port;
	time_t          ck_expiry;
	unsigned int    ck_secure;

	unsigned int    ck_version;
};


static int   http_cookie_gettok       (const char **, char *, size_t);
static int   http_cookie_parse_avpair (const char **, char *, size_t, char *,
					size_t);



/*
 * http_cookie_alloc()
 *
 * Allocate a new structure representing an HTTP cookie and return a
 * reference to the new structure on success, or NULL on failure.
 */
http_cookie_t*
http_cookie_alloc(void)
{
	http_cookie_t *cookie;

	if ((cookie = cnp_malloc(sizeof(*cookie))) == NULL) {
		http_log_errno("failed to allocate cookie structure");
		return (NULL);
	}
	memset(cookie, 0, sizeof(*cookie));

	cookie->ck_version = 1;
	cookie->ck_port = HTTP_PORT;

	return (cookie);
}

/*
 * http_cookie_parse()
 *
 * Parse the string found in <src>, which is expected to contain a cookie
 * encoded according to the format used in HTTP headers.
 * Returns a pointer to the structure representing the parsed cookie
 * on success, or NULL on failure.
 */
http_cookie_t*
http_cookie_parse(const char *src)
{
	int ret;
	time_t expiry;
	char abuf[64], vbuf[1024];
	const char *sp;
	http_cookie_t *cookie;

	if ((cookie = http_cookie_alloc()) == NULL)
		return (NULL);

	sp = src;

	/* parse the name and value of the cookie itself */
	ret = http_cookie_parse_avpair(&sp, abuf, sizeof(abuf),
	    vbuf, sizeof(vbuf));
	if (ret == -1) {
		http_cookie_free(cookie);
		return (NULL);
	}

	if (http_cookie_setname(cookie, abuf) == -1) {
		http_cookie_free(cookie);
		return (NULL);
	}

	if (http_cookie_setval(cookie, vbuf) == -1) {
		http_cookie_free(cookie);
		return (NULL);
	}

	while (*sp != '\0') {
		ret = http_cookie_parse_avpair(&sp, abuf, sizeof(abuf),
		    vbuf, sizeof(vbuf));
		if (ret == -1) {
			http_cookie_free(cookie);
			return (NULL);
		}

		if (strcasecmp(abuf, "comment") == 0) {
		}
		else if (strcasecmp(abuf, "domain") == 0)
			ret = http_cookie_setdomain(cookie, vbuf);
		else if (strcasecmp(abuf, "expires") == 0) {
			ret = http_date_parse(vbuf, HTTP_DATE_GUESS, &expiry);
			if (ret == 0)
				ret = http_cookie_setexpiry(cookie, expiry);
		}
		else if (strcasecmp(abuf, "path") == 0)
			ret = http_cookie_setpath(cookie, vbuf);
		else if (strcasecmp(abuf, "Version") == 0) {
		}
	}

	return (cookie);
}

/*
 * http_cookie_parse_avpair()
 *
 */
static int
http_cookie_parse_avpair(const char **src, char *attr, size_t alen, char *val,
    size_t vlen)
{
	char buf[32];
	int type;

	if ((type = http_cookie_gettok(src, attr, alen)) == TOK_INVAL)
		return (-1);

	if (type != TOK_STRING) {
		http_log_err("unexpected token `%s'", attr);
		return (-1);
	}

	if ((type = http_cookie_gettok(src, buf, sizeof(buf))) == TOK_INVAL)
		return (-1);

	if (type == TOK_SCOL) {
		/* no value specified */
		val[0] = '\0';
	}
	else if (type == TOK_EQUAL) {
		if ((type = http_cookie_gettok(src, val, vlen)) == TOK_INVAL)
			return (-1);

		if ((type != TOK_STRING) && (type != TOK_QSTRING)) {
			http_log_err("unexpected token");
			return (-1);
		}

		type = http_cookie_gettok(src, buf, sizeof(buf));
		if (type == TOK_INVAL)
			return (-1);

		else if ((type != TOK_SCOL) && (type != TOK_END)) {
			http_log_err("unexpected token `%s'", buf);
			return (-1);
		}
	}

	return (0);
}


/*
 * http_cookie_gettok()
 *
 */
static int
http_cookie_gettok(const char **src, char *dst, size_t len)
{
	int type = TOK_INVAL;
	char *dp, *ep;
	const char *sp;

	sp = *src;
	dp = dst;

	/* <ep> points to the last byte of the <dst> buffer */
	ep = dst + len - 1;

	/* skip leading whitespace */
	while (*sp == ' ')
		sp++;

	if (*sp == '\0') {
		type = TOK_END;
	}
	else if (http_istokchar(*sp)) {
		type = TOK_STRING;
		while (http_istokchar(*sp)) {
			*(dp++) = *(sp++);

			if (dp == ep) {
				http_log_err("token too long");
				/* token too long */
				type = TOK_INVAL;
				break;
			}
		}
	}
	else if (*sp == '"') {
		sp++;
		type = TOK_QSTRING;

		while (*sp != '"') {
			*(dp++) = *(sp++);
			if (dp == ep) {
				/* token too long */
				http_log_err("token too long");
				type = TOK_INVAL;
				break;
			}
		}

		sp++;
	}
	else if (*sp == '=') {
		type = TOK_EQUAL;
		*(dp++) = *(sp++);	
	}
	else if (*sp == ';') {
		type = TOK_SCOL;
		*(dp++) = *(sp++);
	}
	else {
		http_log_err("unexpected character `%c' found", *sp);
	}

	*dp = '\0';
	*src = sp;

/*
	printf("TOKEN [%d]: `%s'\n", type, dst);
*/

	return (type);
}

/*
 * http_cookie_free()
 *
 * Free a cookie structure previously allocated with http_cookie_alloc().
 */
void
http_cookie_free(http_cookie_t *cookie)
{
	if (cookie->ck_name != NULL)
		cnp_free(cookie->ck_name);

	if (cookie->ck_domain != NULL)
		cnp_free(cookie->ck_domain);

	if (cookie->ck_value != NULL)
		cnp_free(cookie->ck_value);

	if (cookie->ck_path != NULL)
		cnp_free(cookie->ck_path);

	cnp_free(cookie);
}

/*
 * http_cookie_format()
 *
 */
int
http_cookie_format(const http_cookie_t *cookie, char *buf, size_t len)
{
	snprintf(buf, len, "%s=%s", cookie->ck_name, cookie->ck_value);

	if (cookie->ck_domain != NULL) {
		strlcat(buf, "; domain=", len);
		strlcat(buf, cookie->ck_domain, len);
	}

	if (cookie->ck_path != NULL) {
		strlcat(buf, "; path=", len);
		strlcat(buf, cookie->ck_path, len);
	}

	if (cookie->ck_expiry != 0) {
		strlcat(buf, "; expires=", len);
		strlcat(buf, cookie->ck_domain, len);
	}

	return (0);
}

/*
 * http_cookie_free()
 *
 * Free a cookie structure previously allocated with http_cookie_alloc().
 */
/*
 * http_cookie_getname()
 *
 * Returns the name associated with the HTTP cookie <cookie>.
 */
const char*
http_cookie_getname(const http_cookie_t *cookie)
{
	return (cookie->ck_name);
}

/*
 * http_cookie_getval()
 *
 * Returns the value associated with the HTTP cookie <cookie>.
 */
const char*
http_cookie_getval(const http_cookie_t *cookie)
{
	return (cookie->ck_value);
}

/*
 * http_cookie_getdomain()
 *
 * Returns the domain associated with the HTTP cookie <cookie>.
 */
const char*
http_cookie_getdomain(const http_cookie_t *cookie)
{
	return (cookie->ck_domain);
}

/*
 * http_cookie_getpath()
 *
 * Returns the path associated with the HTTP cookie <cookie>.
 */
const char*
http_cookie_getpath(const http_cookie_t *cookie)
{
	return (cookie->ck_path);
}

/*
 * http_cookie_getexpiry()
 *
 * Returns the value of the expiration date and time for the cookie <cookie>.
 * If a value of 0 is returned, the cookie does not have an expiration date.
 */
time_t
http_cookie_getexpiry(const http_cookie_t *cookie)
{
	return (cookie->ck_expiry);
}

/*
 * http_cookie_issecure()
 *
 * Returns the value of the secure flag as set on the cookie <cookie>.  If
 * this flag is set to a true value, the cookie should only be sent over
 * encrypted connections.
 */
int
http_cookie_issecure(const http_cookie_t *cookie)
{
	return (cookie->ck_secure);
}

/*
 * http_cookie_setname()
 *
 * Set the name associated with the HTTP cookie <cookie> to the value
 * specified in <name>.  A cookie must ALWAYS have a valid name, so this
 * function does not accept NULL as a name value.
 *
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setname(http_cookie_t *cookie, const char *name)
{
	char *tmp;

	if ((tmp = cnp_strdup(name)) == NULL) {
		http_log_errno("failed to allocate new cookie name");
		return (-1);
	}

	if (cookie->ck_name != NULL)
		cnp_free(cookie->ck_name);
	cookie->ck_name = tmp;

	return (0);
}

/*
 * http_cookie_setval()
 *
 * Set the value associated with the HTTP cookie <cookie> to the value
 * specified in <value>.
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setval(http_cookie_t *cookie, const char *value)
{
	char *tmp;

	if ((tmp = cnp_strdup(value)) == NULL) {
		http_log_errno("failed to allocate new cookie value");
		return (-1);
	}

	if (cookie->ck_value != NULL)
		cnp_free(cookie->ck_value);
	cookie->ck_value = tmp;

	return (0);
}

/*
 * http_cookie_setdomain()
 *
 * Set the domain associated with the HTTP cookie <cookie> to the value
 * specified in <domain>.
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setdomain(http_cookie_t *cookie, const char *domain)
{
	char *tmp = NULL;

	if ((domain != NULL) && ((tmp = cnp_strdup(domain)) == NULL)) {
		http_log_errno("failed to allocate new cookie domain");
		return (-1);
	}

	if (cookie->ck_domain != NULL)
		cnp_free(cookie->ck_domain);

	http_log_debug("setting cookie domain to `%s'", tmp);
	cookie->ck_domain = tmp;

	return (0);
}

/*
 * http_cookie_setpath()
 *
 * Set the path associated with the HTTP cookie <cookie> to the value
 * specified in <path>.
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setpath(http_cookie_t *cookie, const char *path)
{
	char *tmp = NULL;

	if ((path != NULL) && ((tmp = cnp_strdup(path)) == NULL)) {
		http_log_errno("failed to allocate new cookie path");
		return (-1);
	}

	if (cookie->ck_path != NULL)
		cnp_free(cookie->ck_path);

	http_log_debug("setting cookie path to `%s'", tmp);
	cookie->ck_path = tmp;

	return (0);
}

/*
 * http_cookie_setexpiry()
 *
 * Set the expiration date and time associated with the HTTP cookie <cookie>
 * to the value specified in <expiry>.
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setexpiry(http_cookie_t *cookie, time_t expiry)
{
	cookie->ck_expiry = expiry;
	return (0);
}

/*
 * http_cookie_setsecure()
 *
 * Set the secure flag associated with the HTTP cookie <cookie> to 1 if the
 * value of <secure> evaluates to true, or 0 otherwise.
 * Returns 0 on success, or -1 on failure.
 */
int
http_cookie_setsecure(http_cookie_t *cookie, int secure)
{
	cookie->ck_secure = (secure > 0) ? 1 : 0;
	return (0);
}
